﻿using System;
using System.Linq;
using System.Reflection;
using Cjc.ExpressionEngine.Excel.Runtime;
using Cjc.ExpressionEngine.Excel.Runtime.Binding;
using Cjc.ExpressionEngine.Excel.Runtime.Types;
using Microsoft.Linq.Expressions;
using Microsoft.Scripting.Ast;
using Microsoft.Scripting.Actions;
using Microsoft.Scripting;
using System.Collections.Generic;

namespace Cjc.ExpressionEngine.Excel.Compiler
{
	public partial class ParseTree
	{
		private ExcelContext context;
		private Expression worksheet;

		private OperationBinder addBinder;
		private OperationBinder subtractBinder;
		private OperationBinder multiplyBinder;
		private OperationBinder divideBinder;
		private OperationBinder concatenateBinder;
		private Dictionary<SymbolId, CellBinder> cellBinders = new Dictionary<SymbolId, CellBinder>();

		public Expression GetExpression( ExcelContext context )
		{
			this.context = context;

			this.worksheet = Expression.Property(
				Expression.ConvertHelper( Expression.Property( Utils.CodeContext(), "Scope" ), typeof( ExcelScope ) ),
				"Worksheet" );

			var codeContext = Utils.CodeContext();
			this.addBinder = new OperationBinder( context.Binder as ExcelBinder, codeContext, "Add" );
			this.subtractBinder = new OperationBinder( context.Binder as ExcelBinder, codeContext, "Subtract" );
			this.multiplyBinder = new OperationBinder( context.Binder as ExcelBinder, codeContext, "Multiply" );
			this.divideBinder = new OperationBinder( context.Binder as ExcelBinder, codeContext, "Divide" );
			this.concatenateBinder = new OperationBinder( context.Binder as ExcelBinder, codeContext, "Concatenate" );

			return GetNodeExpression( this );
		}

		private Expression GetNodeExpression( ParseNode node )
		{
			switch ( node.Token.Type )
			{
				case TokenType.Start:
					return GetStartExpression( node );

				case TokenType.Assignment:
					return GetAssignmentExpression( node );

				case TokenType.AddExpr:
					return GetAddExpression( node );

				case TokenType.MultExpr:
					return GetMultiplyExpression( node );

				case TokenType.Atom:
					return GetAtomExpression( node );

				case TokenType.Range:
					return GetRangeExpression( node );

				case TokenType.TRUE:
					return Expression.True();

				case TokenType.FALSE:
					return Expression.False();

				case TokenType.STRING:
					return GetString( node );

				case TokenType.INTEGER:
					{
						int iVal;
						var value = node.Token.Text;

						return int.TryParse( value, out iVal )
							? Expression.Constant( iVal )
							: Expression.Constant( long.Parse( value ) );
					}

				case TokenType.NUMBER:
					return Expression.Constant( decimal.Parse( node.Token.Text ) );

				case TokenType.IDENTIFIER:
					return GetIdentifierExpression( node );

				case TokenType.Method:
					return GetMethodExpression( node );

				default:
					return Expression.Constant( node.Token.Text );
			}

			return Expression.Constant( null );
		}

		private Expression GetStartExpression( ParseNode node )
		{
			return GetNodeExpression( node.Nodes[ 0 ] );
		}

		private Expression GetAssignmentExpression( ParseNode node )
		{
			return Expression.Assign(
				GetCell( SymbolTable.StringToCaseInsensitiveId( node.Nodes[ 0 ].Token.Text ) ),
				Expression.ConvertHelper( GetNodeExpression( node.Nodes[ 2 ] ), typeof( object ) ) );
		}

		private Expression GetAddExpression( ParseNode node )
		{
			OperationBinder oper = null;
			Expression expr = null;

			foreach ( var n in node.Nodes )
			{
				if ( n.Token.Type == TokenType.PLUSMINUS )
				{
					oper = ( n.Token.Text == "+" )
						? addBinder
						: ( n.Token.Text == "-" )
							? subtractBinder
							: concatenateBinder;
				}
				else
				{
					var next = GetNodeExpression( n );

					expr = ( expr == null )
						? next
						: Expression.Dynamic( oper, typeof( object ), expr, next );
				}
			}

			return expr;
		}

		private Expression GetMultiplyExpression( ParseNode node )
		{
			OperationBinder oper = null;
			Expression expr = null;

			foreach ( var n in node.Nodes )
			{
				if ( n.Token.Type == TokenType.MULTDIV )
				{
					oper = ( n.Token.Text == "*" ) ? multiplyBinder : divideBinder;
				}
				else
				{
					var next = GetNodeExpression( n );

					expr = ( expr == null )
						? next
						: Expression.Dynamic( oper, typeof( object ), expr, next );
				}
			}

			return expr;
		}

		private Expression GetAtomExpression( ParseNode node )
		{
			foreach ( var n in node.Nodes )
			{
				if ( n.Token.Type != TokenType.BROPEN && n.Token.Type != TokenType.BRCLOSE ) return GetNodeExpression( n );
			}

			return null;
		}

		private Expression GetIdentifierExpression( ParseNode node )
		{
			return GetCell( SymbolTable.StringToCaseInsensitiveId( node.Token.Text ) );
		}

		private Expression GetRangeExpression( ParseNode node )
		{
			var cellIds = ( from n in node.Nodes
							where n.Token.Type == TokenType.CELLID
							select new CellId( n.Token.Text ) ).ToArray();

			if ( cellIds.Length > 1 )
			{
/*				return Expression.Dynamic(
					valueConvertBinder,
					typeof( object ),
					Expression.Constant( new Range( cellIds ) ) );*/

				return Expression.Call(
					Expression.Constant( new Range( cellIds ) ),
					typeof( Range ).GetMethod( "GetCells", BindingFlags.Instance | BindingFlags.Public ),
					worksheet );
			}

			return GetCell( cellIds.First().Symbol );
		}

		private Expression GetString( ParseNode node )
		{
			var text = node.Token.Text;

			return Expression.Constant( text.Substring( 1, text.Length - 2 ) );
		}

		private Expression GetMethodExpression( ParseNode node )
		{
			var parameters = ( from n in node.Nodes
							   where n.Token.Type == TokenType.Params
							   from p in n.Nodes
							   where p.Token.Type == TokenType.Param
							   select GetNodeExpression( p.Nodes.First() ) ).ToArray();

			var argIndex = 0;
			var args = parameters.Select( p => Expression.PositionalArg( argIndex++ ) ).ToArray();
			var name = node.Nodes.First().Token.Text;

			return Expression.Dynamic(
				context.CreateCallBinder( name, true, args ),
				typeof( object ),
				new[] { Utils.CodeContext() }.Union( parameters ).ToArray() );
		}

		private Expression GetCell( SymbolId symbol )
		{
			return Expression.Property(
				Expression.Call(
					worksheet,
					typeof( ICellContext ).GetMethod( "GetCell", BindingFlags.Instance | BindingFlags.Public ),
					Expression.Constant( symbol ) ),
				"Value" );
		}
	}
}